﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace Microsoft.Azure.E2E.Common.Net
{
    /// <summary>
    /// A stream comprised of one or more other streams
    /// </summary>
    public class MultiStream : Stream
    {
        private readonly List<Stream> _streams = new List<Stream>();

        private readonly Dictionary<long, int> _offsetBoundaryToStreamMapping = new Dictionary<long, int>();

        /// <summary>
        /// Appends a stream to the multistream
        /// </summary>
        /// <param name="stream">The stream to include</param>
        /// <returns>The current instance of the <see cref="MultiStream"/></returns>
        /// <remarks>The supplied <paramref name="stream"/> must be readable and seekable</remarks>
        public MultiStream AddStream(Stream stream)
        {
            if (!stream.CanRead || !stream.CanSeek)
            {
                throw new ArgumentException("Stream must be readable and seekable", "stream");
            }

            _offsetBoundaryToStreamMapping[0] = 0;
            _offsetBoundaryToStreamMapping[Length + stream.Length] = _offsetBoundaryToStreamMapping.Count;
            _streams.Add(stream);
            return this;
        }

        /// <summary>
        /// Not supported
        /// </summary>
        /// <exception cref="NotSupportedException"></exception>
        public override void Flush()
        {
            throw new NotSupportedException("Stream cannot be flushed as it is not writable");
        }

        /// <summary>
        /// Reads from the stream
        /// </summary>
        /// <param name="buffer">The buffer to read data from the stream into</param>
        /// <param name="offset">The offset in the buffer at which to begin storing the data</param>
        /// <param name="count">The number of bytes to read from the stream</param>
        /// <returns>The number of bytes read from the stream</returns>
        public override int Read(byte[] buffer, int offset, int count)
        {
            if (count == 0)
            {
                return 0;
            }

            var position = Position;
            var key = _offsetBoundaryToStreamMapping.Keys.Where(x => x <= position).Max();
            var streamIndex = _offsetBoundaryToStreamMapping[key];

            if (streamIndex == _offsetBoundaryToStreamMapping.Count - 1)
            {
                return 0;
            }

            var stream = _streams[streamIndex];
            var offsetInStream = position - key;
            var nRead = 0;
            var leftToRead = buffer.Length - offset;

            if (leftToRead > count)
            {
                leftToRead = count;
            }

            while (leftToRead > 0)
            {
                stream.Position = offsetInStream;
                var read = stream.Read(buffer, offset + nRead, leftToRead);
                nRead += read;
                leftToRead -= read;

                if (leftToRead > 0)
                {
                    if (++streamIndex == _streams.Count)
                    {
                        break;
                    }

                    stream = _streams[streamIndex];
                }
            }

            Position += nRead;
            return nRead;
        }

        /// <summary>
        /// Seeks to a particular location in the stream
        /// </summary>
        /// <param name="offset">The distance from the <paramref name="origin"/> to seek</param>
        /// <param name="origin">The position to seek from in the stream</param>
        /// <returns>The new position in the stream</returns>
        /// <remarks>Positive values for <paramref name="offset"/> seek in the direction of the end of the stream (from the beginning of it); negative values should be used when supplying <see cref="SeekOrigin.End"/> for <paramref name="origin"/></remarks>
        public override long Seek(long offset, SeekOrigin origin)
        {
            var position = Position;

            switch (origin)
            {
                case SeekOrigin.Begin:
                    position = offset;
                    break;
                case SeekOrigin.Current:
                    position += offset;
                    break;
                case SeekOrigin.End:
                    position = Length + offset;
                    break;
                default:
                    throw new ArgumentException("Invalid origin", "origin");
            }

            if (position < 0)
            {
                Position = 0;
            }
            else if (position > Length)
            {
                Position = Length;
            }
            else
            {
                Position = position;
            }

            return Position;
        }

        /// <summary>
        /// Not supported
        /// </summary>
        /// <exception cref="NotSupportedException"></exception>
        public override void SetLength(long value)
        {
            throw new NotSupportedException("Setting length is not supported");
        }

        /// <summary>
        /// Not supported
        /// </summary>
        /// <exception cref="NotSupportedException"></exception>
        public override void Write(byte[] buffer, int offset, int count)
        {
            throw new NotSupportedException("Stream is not writable");
        }

        /// <summary>
        /// Gets whether or not the stream can be read
        /// </summary>
        /// <remarks>Always returns true</remarks>
        public override bool CanRead
        {
            get { return true; }
        }

        /// <summary>
        /// Gets whether or not the stream can be seeked in
        /// </summary>
        /// <remarks>Always returns true</remarks>
        public override bool CanSeek
        {
            get { return true; }
        }

        /// <summary>
        /// Gets whether or not the stream may be written to
        /// </summary>
        /// <remarks>Always returns false</remarks>
        public override bool CanWrite
        {
            get { return false; }
        }

        /// <summary>
        /// Gets the length of the stream
        /// </summary>
        /// <remarks>This is the sum of the lengths of the component streams</remarks>
        public override long Length
        {
            get { return _offsetBoundaryToStreamMapping.Keys.Max(); }
        }

        /// <summary>
        /// Gets or sets the current position in the stream
        /// </summary>
        public override long Position { get; set; }

        /// <summary>
        /// Frees any resources the stream is currently using
        /// </summary>
        /// <remarks>Calls <see cref="IDisposable.Dispose"/> on all the component streams</remarks>
        protected override void Dispose(bool disposing)
        {
            foreach (var stream in _streams)
            {
                stream.Dispose();
            }
        }
    }
}